﻿using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Diagnostics;

namespace FKRP.WP7.Library.SkeletonTracker
{
    public class Client : IDisposable
    {
        private int _port;
        private string _serverName = string.Empty;
        StringBuilder _responseBuffer = new StringBuilder();

        SocketAsyncEventArgs _socketEventArg;

        public event EventHandler<ClientEventArgs> ClientEvent;
        public event EventHandler<ReceivedTrackingDataEventArgs> ReceivedTrackingData;

        private Client() { }
        public Client(string hostName, int port)
        {
            if (String.IsNullOrWhiteSpace(hostName))
                throw new ArgumentNullException("serverName");

            if (port < 0 || port > 65535)
                throw new ArgumentNullException("portNumber");

            _serverName = hostName;
            _port = port;
        }

        private void SendReceivedTrackingData(String rawJSON)
        {
            if (ReceivedTrackingData != null)
            {
                System.Windows.Deployment.Current.Dispatcher.BeginInvoke(() =>
                {
                    var args = new ReceivedTrackingDataEventArgs() { SkeletonData = Avatar.Create(rawJSON) };
                    ReceivedTrackingData(this, args);
                });
            }
        }

        private void SendClientEvent(ClientEventTypes eventType, string body)
        {
            if (ClientEvent != null)
            {   
                System.Windows.Deployment.Current.Dispatcher.BeginInvoke(() =>
                {                    
                    var args = new ClientEventArgs(eventType, false, body);
                    ClientEvent(this, args);
                });
            }        
        }

        private void SendClientError(ClientEventTypes eventType, string message)
        {
            if (ClientEvent != null)
            {
                System.Windows.Deployment.Current.Dispatcher.BeginInvoke(() =>
                {
                    var args = new ClientEventArgs(eventType, true, message);
                    ClientEvent(this, args);
                });
            }
        }        

        public void Connect()
        {
            _socketEventArg = new SocketAsyncEventArgs() { RemoteEndPoint = new DnsEndPoint(_serverName, _port)};
            _socketEventArg.Completed += (sndr, args) =>
            {
                switch (args.LastOperation)
                {
                    case SocketAsyncOperation.Connect: ProcessConnect(args); break;
                    case SocketAsyncOperation.Receive: ProcessReceive(args); break;
                    case SocketAsyncOperation.Send: ProcessSend(args); break;
                    default: throw new Exception("Invalid operation completed");
                }
            };
            
            _socketEventArg.UserToken = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) { ReceiveBufferSize = 1024 }; ;
            (_socketEventArg.UserToken as Socket).ConnectAsync(_socketEventArg);
        }

        private void ProcessReceiveExtracted(int nullTerminator, String dataFromServer)
        {
            dataFromServer = dataFromServer.Trim('\0');

            _responseBuffer.Append(dataFromServer.Substring(0, nullTerminator));
            var message = _responseBuffer.ToString();
            if (message.StartsWith("{") && message.EndsWith("}"))
            {
                _responseBuffer = new StringBuilder();
                if (nullTerminator < dataFromServer.Length)
                    _responseBuffer.Append(dataFromServer.Substring(nullTerminator).TrimStart('\0'));

                SendReceivedTrackingData(message.ToString());
            }
            else
                SendClientError(ClientEventTypes.Receive, "Invalid Skeleton Data.");
        }        

        private void ProcessReceive(SocketAsyncEventArgs e)
        {
            if (e.SocketError == SocketError.Success)
            {
                var nullTerminator = -1;
                for (var idx = 0; idx < e.BytesTransferred; ++idx)
                {
                    if (e.Buffer[idx] == 0x00)
                    {
                        nullTerminator = idx;
                        break;
                    }
                }

                var dataFromServer = Encoding.UTF8.GetString(e.Buffer, 0, e.BytesTransferred);

                //Found null terminator somewhere in message, process the data.
                if (nullTerminator != -1)
                    ProcessReceiveExtracted(nullTerminator, dataFromServer);
                else //Otherwise append the string to the response buffer.
                    _responseBuffer.Append(dataFromServer);
                    
                (e.UserToken as Socket).ReceiveAsync(e);
            }
            else
                SendClientError(ClientEventTypes.Receive, e.SocketError.ToString());
        }

        private void ProcessSend(SocketAsyncEventArgs e)
        {
            if (e.SocketError == SocketError.Success)
            {                
                Debug.WriteLine("Sent data.");                
                (e.UserToken as Socket).ReceiveAsync(e);
            }
            else
                SendClientError(ClientEventTypes.Send, e.SocketError.ToString());
        }

        private void ProcessConnect(SocketAsyncEventArgs e)
        {
            if (e.SocketError == SocketError.Success)
            {
                Debug.WriteLine("Socket is connected on the device!");
                SendClientEvent(ClientEventTypes.Connect, string.Empty);

                // Successfully connected to the server, then send CONNECT<EOF>
                // V1.0 of library is transmit only
                byte[] buffer = Encoding.UTF8.GetBytes("CONNECT<EOF>");
                e.SetBuffer(buffer, 0, buffer.Length);               
                (e.UserToken as Socket).SendAsync(e);
            }
            else
                SendClientError(ClientEventTypes.Connect, e.SocketError.ToString());
        }

        public void Dispose()
        {
            var socket = (_socketEventArg.UserToken as Socket);

            if (socket != null)
            {
                socket.Close();
                socket.Dispose();
            }

            if (_socketEventArg != null)
                _socketEventArg.Dispose();
        }
    }
}